接下來我們來看看在SwiftUI 怎麼使用WebView 網頁的元件,SwiftUI 框架 有一個缺點,就是它未能提供所有 UIKit 有的 UI 控件,例如說由於 SwiftUI 還沒有定義 WebView,所以Apple 有一個 UIViewRepresentable 協定,讓你可以輕鬆打包 (wrap) 一個 UIView,並讓 SwiftUI 專案使用,以下我們就將傳統的 UI 元件包裝成 SwiftUI view,並使用 WKWebView 來加載 Web 應用程序,讓我們照著以下步驟來完成
在 SwiftUI 中使用 UIKit 中的任何組件都需要用 UIViewRepresentable 包裝
首先我們先新增一個 Swift File:並命名為 WebView.swift
,裡面內容為struct WebView,並實作UIViewRepresentable
協定
import SwiftUI
import WebKit
struct SwiftUIWebView: UIViewRepresentable {
}
實作 UIViewRepresentable 表示此 struct 定義的WebView,是有著 UIView 元件特性的 SwiftUI view,所以它可以包裝 UICollectionView,WKWebView,MKMapView 等 UI 元件,待會我們將用它包裝 WKWebView
要符合 UIViewRepresentable
協定,需實作兩個方法,makeUIView(context:)
以及updateUIView(_:context:)
在程式區塊上建立makeUIView(context:)
方法,可經由Xcode 能自動完成
再把makeUIView
方法裡回傳的型別改成我們要用的WKWebView
func makeUIView(context: Context) -> WKWebView {
}
同樣的方式,建立updateUIView(_:context:)
方法,記得把參數uiView
改成我們要用的WKWebView
func updateUIView(_ uiView: WKWebView, context: Context) {
}
現在我們來完成以上這兩個方法的實作,首先makeUIView
裡我們要回傳 WKWebView 的物件,這邊可以透過WKWebpagePreferences
以及WKWebViewConfiguration
來對我們的WebView 設置一些偏好設定
範例:
func makeUIView(context: Context) -> WKWebView {
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = false
let config = WKWebViewConfiguration()
config.defaultWebpagePreferences = prefs
return WKWebView(
frame: .zero,
configuration: config
)
}
接下來是updateUIView
,它將在 SwiftUI view 畫面更新時被呼叫,我們可以在 WebView 裡宣告一個屬性來由外部傳入網址,然後在生成 WebView 時傳入內容,在 updateUIView 才載入網頁
如下:
struct WebView: UIViewRepresentable {
let url: URL?
func makeUIView(context: Context) -> WKWebView {
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = false
let config = WKWebViewConfiguration()
config.defaultWebpagePreferences = prefs
return WKWebView(
frame: .zero,
configuration: config
)
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard let myURL = url else {
return
}
let request = URLRequest(url: myURL)
uiView.load(request)
}
}
接下來我們就可以使用我們的WebView 來載入我們要的網頁了
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.youtube.com/"))
}
}
目前我們只討論了 UIViewRepresentable
協定中的幾種方法。如果你需要在 UIKit 中使用委託 (delegate) 並與 SwiftUI 溝通,就必須實現 makeCoordinator
方法,並提供一個 Coordinator
實例。Coordinator
是 UIView 的委託 和 SwiftUI 之間的橋樑
我們在WebView struct 中,創建一個 Coordinator
類別並採用WKNavigationDelegate
協定來處理webView 的導航事件,並在 makeCoordinator
方法回傳其實例,可搭配數據流等來做資料的傳遞
範例:
struct WebView: UIViewRepresentable {
@Binding var title: String
var url: URL
var loadStatusChanged: ((Bool, Error?) -> Void)? = nil
func makeCoordinator() -> WebView.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
view.load(URLRequest(url: url))
return view
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// you can access environment via context.environment here
// Note that this method will be called A LOT
}
func onLoadStatusChanged(perform: ((Bool, Error?) -> Void)?) -> some View {
var copy = self
copy.loadStatusChanged = perform
return copy
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
parent.loadStatusChanged?(true, nil)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.title = webView.title ?? ""
parent.loadStatusChanged?(false, nil)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
parent.loadStatusChanged?(false, error)
}
}
}
struct ContentView: View {
@State var title: String = ""
@State var error: Error? = nil
var body: some View {
WebView(title: $title, url: URL(string: "https://www.apple.com/")!)
.onLoadStatusChanged { loading, error in
if loading {
print("Loading started")
self.title = "Loading…"
}
else {
print("Done loading.")
if let error = error {
self.error = error
if self.title.isEmpty {
self.title = "Error"
}
}
else if self.title.isEmpty {
self.title = "Some Place"
}
}
}
}
}